数据结构与算法 C++数据结构与算法
my::数据结构与算法
指针的引用
可视化数据结构与算法(usfca.edu)
〇、绪论 1 数据结构
术语:
数据:信息的载体。
数据元素:数据的基本单位,由若干个数据项组成。
数据对象:同性质元素集合。
数据类型:原子类型,结构类型,抽象数据类型(类)。
数据结构三要素:
逻辑结构:线性结构,非线性结构。
存储结构:顺序存储,链式存储,索引存储,散列存储。
2 算法
算法评估:
性质:有穷性,确定性,可行性,输入,输出。
好算法:正确性,可读性,健壮性,高效率与低储存量。
时间复杂度:T( n ) = O( f(n) )
时间复杂度数学定义:若 T(n) 和 f(n) 是定义再正整数集合上的两个函数,则存在正常数 C 和 n0,使得当 n >= n0 时,都满足 0 <= T(n) <= Cf(n)
时间复杂度比较:O(1) < O(log2n) < O(n) < O(nlog2n) < O(n**2) < O(n**3) < O(2**n) < O(n!) < O(n**n)
空间复杂度:S( n ) = O( g( n ) ),即辅助空间。
一、线性表 1 线性表
线性存储的一种数据结构。
struct {elem[],length};struct {*data,length};void InitList (SqList& L) ;int Length (SqList L) ;int LocateElem (SqList L, ElemType e) ;int GetElem (SqList L, int i) ;bool ListInsert (SqList& L,int i,ElemType e) ;bool ListDelete (SqList& L,int i,ElemType& e) ;void PrintList (SqList L) ;bool Empty (SqList L) ;void DestroyList (SqList& L) ;
2 链表
线性存储数据,存储的数据地址不连续。
2.1 单链表 struct {data,*next};LinkList List_HeadInsert (LinkList &L) ;LinkList List_TailInsert (LinkList &L) ;Lode* GetElem (LinkList L,int i) ;Lode* LocateElem (LinkList L,ElemType e) ;
2.2 双向链表 struct {data,*next,*prior};
2.3 循环链表
循环单链表:基本同理,但判空条件不是 *next 指向 null , 而是是否等于头指针。(指向头结点)
循环双链表:基本同理,空表时 next 和 prior 都指向头结点。
2.4 静态链表
3 一元多项式
用计算机表示一元多项式。
typedef struct LNode { float coef; int expn; struct LNode * next; }pnode, *polynomial;
二、栈、队列和数组 1 栈
先进后出的线性表。
特性:
栈的应用:
1.1 顺序栈 struct {data[],*next};void InitStack (SqStack &S) ;bool StackEmpty (SqStack S) ;bool Push (SqStack &S,ElemType x) ;bool Pop (SqStack &S , ElemType &x) ;bool GetTop (SqStack S , ElemType &x) ;
1.2 其他栈 (top0 == -1 ) && (top1 == MaxSize) (top1 - top0) == 1 struct {data,*next};
2 队列
先进先出的线性表。
顺序队列:
struct { data[] , front , rear };
栈空:front = rear = 0
rear == MaxSize
不能判断队满
循环队列:
链式队列:
结点:struct node {data,*next};
队列:struct queue{*front,*rear};
front == NULL && rear == NULL
队列为空
双端队列:
两端入两端出。
输出受限:两端入一端出。
输入受限:一端入两端出。
void InitQueue (SqQueue &Q) ;bool isEmpty (SqQueue Q) ;bool EnQueue (SqQueue &Q,ElemType x) ;bool DeQueue (SqQueue &Q,ElemType &x) ;bool GetHead (SqQueue Q,ElemType &x) ;
3 数组
维度和维界不变的线性表。
行存储: LOC(a[i,j]) = LOC(a[0,0]) + [i*(col_len+ 1)+j] * size
列存储: LOC(a[i,j]) = LOC(a[0,0]) + [j*(row_len+1)+i] * size
不定参数
4 特殊矩阵
压缩矩阵:多同值元素仅分配一个存储空间,零元素不分配空间。
特殊矩阵:有许多相同元素或零,且分布有一定规律。
矩阵分区:下三角区,主对角线,上三角区。
行优先:先存完该行的每一列。
列优先:先存完该列的每一行。
对称矩阵:
定义:n 阶矩阵任一元素都 a[i,j] = a[j,i]
。一般上三角折到下三角。
三角矩阵:
下三角矩阵:上三角区元素均为同一变量(不包括主对角线),则在线性存储时尾部加一个空间存上三角值。
上三角矩阵:同理。
三对角矩阵:
定义:n 阶矩阵任一元素 a[i,j]
,当 |i - j| > 1
时有 a[i,j] = 0
通俗:首位两行各两个元素,其他行每行只有三个元素,坐标是j = i-1,j = i,j = i+1
稀疏矩阵:
三、串 1 串
存字符的线性表。
struct {ch[],length};struct {*ch,length};void StrAssign (String &T,String chars) ;void StrCopy (String &T,String S) ;bool StrEmpty (String S ) ;bool StrCompare (String S,String T) ;int StrLength (String S) ;void SubString ( String &Sub,String S,int pos,int len) ;void Concat (String &T,String S1,String S2) ;int Index (String S,String T) ;void ClearString (String &S) ;void DestroyString (String &S) ;
2 KMP算法 class Solution {public : int strStr (string haystack, string needle) { int len = haystack.size (); int subLen = needle.size (); int lp = 0 ; int rp = 0 ; int res = -1 ; vector<int > next (subLen, 0 ) ; buildNext (needle, subLen, next); while (lp < len && rp < subLen) { if (haystack[lp] == needle[rp]) { lp++; rp++; } else if (rp > 0 ) { rp = next[rp - 1 ]; } else { lp++; } } if (rp == subLen) { res = lp - rp; } return res; } void buildNext (string needle, int subLen, vector<int >& next) { int prefix_len = 0 ; int i = 1 ; while (i < subLen) { if (needle[prefix_len] == needle[i]) { prefix_len++; next[i] = prefix_len; i++; } else { if (prefix_len != 0 ) { prefix_len = next[prefix_len - 1 ]; } else { next[i] = 0 ; i++; } } } } };
四、树 1 树
分支关系的非线性结构。
任何一个非空树是一个二元组。Tree = ( root , F ):
完美二叉树, 完全二叉树和完满二叉树 - veli - 博客园 (cnblogs.com)
基本概念名称
意义
结点
数据元素 + 若干指向子树的分支
结点的度
分支的个数
树的度
树中所有结点的度的最大值
叶子结点
度等于0的结点
分支结点
度大于0的结点
孩子结点
下一级
双亲结点
上一级
兄弟结点
同级
祖先结点
上级
子孙结点
下级
层次
树的层数
深度
最大层次
森林
多个互不相交的树的集合
2 二叉树
每个结点至多只有两颗子树,且子树有左右之分的树。
二叉树的五种状态:
空
只有跟结点
有根结点和左子树
有根结点和右子树
有跟结点和左右子树
二叉树性质:
在二叉树的第 i 层至多有 2 ^ ( i - 1 )个结点( i >= 1)。
深度为 k 的二叉树上至多有 2 ^ k - 1个结点( k >= 1)。
任何一颗二叉树,他含有n0个叶子结点,n2个度为2的结点,则必然有n0 = n2 + 1。
满二叉树(完美二叉树):深度为 k 且含有 2 ^ k - 1 个结点的二叉树。
完全二叉树:树中所含的 n 个结点和满二叉树中编号为 1至n 的结点一一对应。(从左到右从上到下编号,不一定是满二叉树)
完全二叉树性质(123如上):
具有 n 个结点的完全二叉树的深度为 ⌊log2 n⌋+ 1 。 (⌊⌋ 向下取整)
⌊2i⌋ 为根节点 ,i 的左孩子是 2i , 右孩子是 2i + 1。所以若 2i / 2i + 1 > n 则该结点无左孩子/右孩子。
typedef struct BiTNode { TElemType data; struct BiTNode * lchild, * rchild; }BiTNode, * BiTree; typedef struct TriTNode { TElemType data; struct TriTNode * lchild, * rchild; struct TriTNode * parent; }TriTNode, * TriTree; typedef struct BPTNode { TElemType data; int * parent; char LRTag; }BPTNode; typedef struct BPTree { BPTNode nodes[MAX_TREE_SIZE]; int num_node; int root; }BPTree; SqBiTree[MAX_TREE_SIZE];
五、图
六、算法
text::算法进阶
七、案例 1 日期之差
author::日期之差
int countdays (int y, int m, int d) { if (m < 3 ) y--, m += 12 ; return 365 * y + (y >> 2 ) - y / 100 + y / 400 + (153 * m - 457 ) / 5 + d - 306 ; }
Python数据结构与算法
text::Python
text::Python蓝桥杯题集
一、引论
图灵机:
数据类型的性能:
x = lst[i] lst[i] = x lst.append(x) lst = lst + new_lst
pip install pythonds3 from pythonds3.trees.binary_heap import BinaryHeap
二、栈 1 栈实现
list末端是栈顶,首端是栈底。底不会变,变的是顶的位置。(时间复杂度较低)
class Stack : def __init__ (self ): self.__stack = [] def push (self,item:any )->None : self.__stack.append(item) def pop (self )->any : return self.__stack.pop() def peek (self )->any : return self.__stack[-1 ] def isEmpty (self )->bool : return self.__stack == [] def size (self )->int : return len (self.__stack)
2 表达式求值
三、队列 1 队列实现 class Queue : def __init__ (self ): self.__queue = [] def enqueue (self,item:any )->None : self.__queue.append(item) def dequeue (self )->any : return self.__queue.pop(0 ) def isEmpty (self )->bool : return self.__queue == [] def size (self )->int : return len (self.__queue) class Queue : def __init__ (self ): self.__queue = [] def enqueue (self,item:any )->None : self.__queue.insert(0 ,item) def dequeue (self )->any : return self.__queue.pop() def isEmpty (self )->bool : return self.__queue == [] def size (self )->int : return len (self.__queue)
2 双端队列实现
如果端相反,则操作和时间复杂度都相反。
class Deque : def __init__ (self ): self.__deque = [] def addFront (self,item:any )->None : self.__deque.insert(0 ,item) def addRear (self,item:any )->None : self.__deque.append(item) def removeFront (self )->any : return self.__deque.pop(0 ) def removeRear (self )->any : return self.__deque.pop() def isEmpty (self )->bool : return self.__deque == [] def size (self )->int : return len (self.__deque)
四、链表 1 普通无序链表 class Node : def __init__ (self,intidata:any ): self.__data = intidata self.__next = None def getData (self )->any : return self.__data def getNext (self )->any : return self.__next def setData (self,newdata:any )->None : self.__data = newdata def setNext (self,newnext:any )->None : self.__next = newnext class HeadNode : def __init__ (self ): self.__next = None def getNext (self )->Node: return self.__next def setNext (self,newnext:any )->None : self.__next = newnext def insert (self,item:any ,location:int =1 )->None : new = Node(item) if location == 1 : if self.getNext() != None : new.setNext(self.getNext()) self.setNext(new) else : self.setNext(new) else : n = self.getNext() for i in range (1 ,location - 1 ): n = n.getNext() new.setNext(n.getNext()) n.setNext(new) def peek (self )->list : res = [] if self.getNext() != None : n = self.getNext() while n.getNext() != None : res.append(n.getData()) n = n.getNext() res.append(n.getData()) return res def count (self )->int : res = 0 n = self.getNext() if n != None : while n != None : res += 1 n = n.getNext() return res def reverse (self,head ): prev = None curr = head while curr: next_node = curr.next curr.next = prev prev = curr curr = next_node return prev def remove (self,location:int )->Node: pass def search (self,item:any )->int : pass
2 有序表
定义:按大小顺序排好的链表。
五、散列 1 哈希表
事先确定了数据的位置,查找的时间为:O(1)
def hash( num ):计算哈希值。(一般是求余)
hash_lst对应余数值,查找时计算余数值后在 hash_lst 中寻找是否有该数值。
完美散列函数:每一个数据项都映射到不同的槽中。
好的散列函数:
冲突少(近似完美)
计算难度低(额外开销小)
充分分散数据项(节约空间)
作为一致性校验的数据“指纹”函数需要具备如下的特性:
压缩性:任意长度的数据,得到的“指纹”长度是固定的。
易计算性:从原数据计算“指纹”很容易。(从指纹计算原数据是不可能的)
抗修改性:对原数据的微小变动,都会引起“指纹”的大改变。
抗冲突性:已知原数据和“指纹”,要找到相同指纹的数据(伪造)是非常困难的。
产生了哈希冲突:如果产生了哈希冲突,那么哈希映射的值们会形成一个链表,当链表过长时(长度>8),此时链表又会转化成红黑树。
案例:md5(128位),SHA-0/SHA-1(160位(20 Bytes)),SHA-256,SHA-224,SHA-512,SHA-384
应用:密码,网盘(先上传散列值,比较服务器中有无该资源,有则存链接,无则上传文件)
import hashlibm = hashlib.md5() m.update("password" .encode('utf-8' )) pw = m.hexdigest() print (pw)print (hashlib.md5(("Hello World!" .encode('utf-8' ))).hexdigest())
2 折叠法
将数据项按照位数分为若干段,再将几段数字相加,最后对散列表大小求余。
隔数反转:隔一个数将数字进行反转。(属于一种微调手段,以便更好符合散列特效)
s = "12312311231" h = [0 for i in range (len (s))] gap = 2 count = 0 for i in range (0 ,len (s),gap): i = int (s[i:i+gap-1 ]) count += i h[count % len (s)] = s print (h)
3 平方取中法
先对数进行平方,再取平方数中间两位,最后对散列表大小求余。
4 设计注意
变位词:字母一样,顺序不同。为了防止可以将字符所在位置设为权重相乘。
5 冲突解决
开放定址(open addressing):在冲突的数据项处重新再逐个向后找一个空槽。该方法叫线性探测(linear probing):
缺点:有聚集的趋势。
改进:逐个向后找改为跳跃式探测,例如一次+3。(尽量将散列表大小设为素数,防止周期)
改进:不再固定为一个值,而是逐步增加,例:+1 , +3 , +5 , +7 …
数据项链(Chaining):将槽扩展为数据项集合。(散列 + 排序查找)
6 ADT Map
dict:映射Map。
class HashTable : def __init__ (self,size:int ): self.size = size self.slots = [None ] * self.size self.data = [None ] * self.size def hashfunction (self,key:any )->any : return key % self.size def rehash (self,oldhash:int )->any : return (oldhash + 1 ) % self.size def put (self,key:any ,data:any )->None : hashvalue = self.hashfunction(key) if self.slots[hashvalue] == None : self.slots[hashvalue] = key self.data[hashvalue] = data else : if self.slots[hashvalue] == key: self.data[hashvalue] = data else : nextslot = self.rehash(hashvalue) while self.slots[nextslot] != None and self.slots[nextslot] != key: nextslot = self.rehash(nextslot) if self.slots[nextslot] == None : self.slots[nextslot] = key self.data[nextslot] = data else : self.data[nextslot] = data def get (self,key:any )->any : startslot = self.hashfunction(key) data = None stop = False found = False position = startslot while self.slots[position] != None and not found and not stop: if self.slots[position] == key: found = True data = self.data[position] else : position = self.rehash(position) if position == startslot: stop = True return data def __getitem__ (self, key ): return self.get(key) def __setitem__ (self, key, data ): self.put(key,data)
六、树 1 术语
基本同C语言。
名称
意义
入边
进入该节点的边
出边
离开该节点的边
2 列表嵌套法 myTree = [root,left,right] myTree = [root,first,second,third,...] class myTree : def __init__ (self,r:any ): self.BinaryTree = [r,[],[]] def insertLeft (self,root,newBrance ): t = root.pop(1 ) if len (t) > 1 : root.insert(1 ,[newBrance,t,[]]) else : root.insert(1 ,[newBrance,[],[]]) return root def insertRight (self,root,newBrance ): t = root.pop(2 ) if len (t) > 1 : root.insert(2 ,[newBrance,t,[]]) else : root.insert(2 ,[newBrance,[],[]]) return root
3 节点链接法 class BinaryTree : def __init__ (self,rootObj ): self.key = rootObj self.leftChild = None self.rightChild = None def insertLeft (self,newNode ): if self.leftChild == None : self.leftChild = BinaryTree(newNode) else : t = BinaryTree(newNode) t.leftChild = self.leftChild self.leftChild = t def insertRight (self,newNode ): if self.rightChild == None : self.rightChild = BinaryTree(newNode) else : t = BinaryTree(newNode) t.rightChild = self.rightChild self.rightChild = t
4 表达式树
my::表达式树
求表达式值
例:$((5+((3+7)(4-1)))+(4 5))$
思路:碰到左括号左下降,碰到数字记录后回升,碰到操作数记录后右下降,碰到右括号回升,留一个栈记录父节点。
5 树的遍历
前序遍历(preorder):NLR。先根再递归前序访问左子树,最后前序访问右子树。
中序遍历(inorder):LNR。先递归地中序访问左子树,再访问根,最后中序访问右子树。
后序遍历(postorder):LRN。先递归地后序访问左子树,再后序访问右子树,最后访问根。
class TreeOrder : def preorder (self,tree:BinaryTree ): if tree: print (tree.getRootVal()) self.preorder(tree.getLeftChild()) self.preorder(tree.getRightChild()) def inorder (self,tree:BinaryTree ): if tree: self.inorder(tree.getLeftChild()) print (tree.getRootVal()) self.inorder(tree.getRightChild()) def postorder (self,tree:BinaryTree ): if tree: self.postorder(tree.getLeftChild()) self.postorder(tree.getRightChild()) print (tree.getRootVal()) def preorder (self ): print (self.key) if self.leftChild: self.leftChild.preoder() if self.rightChild: self.rightChild.preorder()
6 二叉堆实现优先队列
my::二叉堆
最小 key 排在队首称为最小堆(min heap),反之是最大堆。(max heap)
实现:完全二叉树。
最小堆(小根堆):任何一个节点 x ,其父节点 p 中的 key 均小于 x 中的 key。
最大堆(大根堆):任何一个节点 x ,其父节点 p 中的 key 均大于 x 中的 key。
时间:
heapq
:Python中heapq模块浅析_heapq.heappush-CSDN博客
题目:
class BinaryHeap : def __init__ (self ): self.heapList = [0 ] self.currentSize = 0 def percUp (self, i:int )->None : while i // 2 > 0 : if self.heapList[i] < self.heapList[i // 2 ]: self.heapList[i],self.heapList[i // 2 ] = self.heapList[i // 2 ],self.heapList[i] i //= 2 def percDown (self,i:int )->None : while i * 2 <= self.currentSize: mc = self.minChild(i) if self.heapList[i] > self.heapList[mc]: self.heapList[i],self.heapList[mc] = self.heapList[mc],self.heapList[i] i = mc def minChild (self,i:int )->int : if i * 2 + 1 > self.currentSize: return i * 2 else : if self.heapList[i * 2 ] < self.heapList[i * 2 + 1 ]: return i * 2 else : return i * 2 + 1 def insert (self,key:any )->None : self.heapList.append(key) self.currentSize += 1 self.percUp(self.currentSize) def findMin (self )->any : if self.size() > 0 : return self.heapList[1 ] else : return None def delMin (self )->any : retval = self.heapList[1 ] self.heapList[1 ] = self.heapList[self.currentSize] self.currentSize -= 1 self.heapList.pop() self.percDown(1 ) return retval def isEmpty (self )->bool : return self.currentSize > 0 def size (self )->int : return self.currentSize def buildHeap (self,lst:list ): i = len (lst) // 2 self.currentSize = len (lst) self.heapList = [0 ] + lst[:] print (len (self.heapList),i) while i > 0 : print (self.heapList,i) self.percDown(i) i -= 1 print (self.heapList,i)
7 二叉查找树
实现:有序表数据结构 + 二分搜索 或 散列表数据结构 + 散列及冲突解决
性质:比父节点小的 key 都出现在左子树,比父节点大的 key 都出现在右子树。
普通二叉查找树最坏:O( log n ),最好:O( n )(单链表),插入时受数据顺序影响,最终会形成不同的二叉查找树。
my::二叉查找树
8 AVL树
平衡二叉查找树。
平衡因子:高度差,大于0左重,小于0右重,若每个节点平衡因子都在 [-1,0,1] 之中则成为平衡树。
时间:O( log n )(类似斐波那契数列)
插入:按照二叉查找树的规则,插入到指定位置,当高度差大于1时,进行旋转。
旋转规则(高度差大于1时):
LL:爷父子,都是左边。此时爷爷变成父亲的右结点(右旋),父亲变成两者父结点。
RR:爷父子,都是右边。此时爷爷变成父亲的左结点(左旋),父亲变成两者父结点。
LR:父在爷的左边,子在父右边。此时父亲先变成孩子的左结点,然后爷爷变成孩子右节点(先左旋再右旋),孩子变成两者父结点。
RL:父在爷的右边,子在父左边。此时父亲先变成孩子的右结点,然后爷爷变成孩子左节点(先右旋再左旋),孩子变成两者父结点。
删除:
删除叶节点:直接删除。
删除的节点只有左子树或右子树:直接删除后让左 / 右子树代替即可。
删除的节点左右子树都有:找到直接前驱或后继(下一个存在于AVL树中比他小或大的数),直接替换,然后删除直接前驱或后继,删除后按照删除规则继续判定。
9 B树
B树:多路平衡查找树。
m阶B树:多叉查找树。树中每个结点至多有m个孩子结点(即至多有m-1个关键字)
比如一个5阶B树,一个节点有:[20, 30, 62, 89]
这样的四个值,那么他会有五个子节点,分别是:<20, >20 and <30, >30 and <62, >62 and <89, >89
B树结构如下:[n,p0,k1,p1,k2, … , kn,pn]
n:结点的关键字个数
p:指针,指向孩子结点
k:关键字,通过关键字来判断孩子的位置。
演示:B-Tree Visualization (usfca.edu)
构建B树:当前结点个数不满足阶树时,直接插入到指定位置。当达到阶树后,比如5阶B树,某个结点原来就有4个值,再插入1个,此时中间值就会向上分裂。比如:[600, 800, 1000, 1200]
,插入1400后,此时会变成根结点是[1000]
,左边指向[600, 800]
,右边指向[1000, 1200]
。依次类推,每次当达到了阶数就会向上分裂。如果是偶数比如4阶就是左边的数为中间值。比如[600, 800, 1000]
,插入1200后,变成根结点是[800]
,左边是[600]
,右边是[800, 1000]
使用场景:B树通常用于数据库和文件系统中,用于实现索引数据结构,可以提高数据库查询的效率,减少磁盘IO次数。
10 B+树
同理于B数,但是B+树数据是全部存在叶子处,节点上只存是数据指针,同时根部会形成链表,从左到右依次连接。
和B数的区别:
所有数据都会出现在叶子节点。
叶子节点会形成一个单向链表。
演示:B+ Tree Visualization (usfca.edu)
构建B树:插入规则同理,只不过在裂变时,会把中间数据值放入叶子节点,同时构建一个链表。
使用场景:B+树也被广泛应用于数据库系统中,用于实现索引结构、范围查询和多级索引,B+树相比B树更适合用于磁盘存储,因为B+树的内部节点只存储键值,并通过指针链接叶子节点。
11 红黑树
自身性质:
二叉搜索树。(左根右)
根和叶子都是黑色。叶子结点是NULL。(根叶黑)
所有的红色结点左右孩子都是黑色的,也就是不存在两个连续的黑色结点。(不红红)
任一结点到叶结点所有途径的黑色结点的数量相同。(黑路同)
特点:
最长路径不超过最短路径的两倍,也就是任一结点左右子树的高度差不超过两倍。相比起AVL树(高度差不超过1),AVL树查询更高效,而红黑树插入和删除更高效。
插入:
插入按照AVL树的规则,插入的节点默认是红色。因为这样只可能违反根叶黑或不红红,相比插入黑色更容易调整。
若性质被破坏,则需要调整,调整规则如下:
插入的是根结点:直接变黑
插入的结点的叔叔是红色:叔父爷变色,同时爷爷变成插入结点继续做判定。
插入的结点的叔叔是黑色:按AVL树旋转规则(LL,RR,LR,RL)旋转后对旋转的两个点变色。
删除:
删除的节点只有左孩子或右孩子:直接删除后让左 / 右子树代替后变黑即可。
没有孩子(不包括空):
author::红黑树 - 删除_哔哩哔哩_bilibili
使用场景:红黑树也被广泛用于实现Map和Set等数据结构(C++中的STL),以及在操作系统中用于实现进程调度、文件系统等,其中最著名的例子是Linux内核中红黑树的应用。
12 多路归并树
13 败者树
七、图(Graph) 1 术语
若有向图中不存在任何圈,则称作 “ 有向无圈图 directed acyclic graph:DAG “(树是一种DAG,该问题更简单)
名称
意义
顶点Vertex(节点Node)
图的基本组成部分,顶点具有名称标识Key,也可以携带数据项payload
边Edge(弧Arc)
边连接两个顶点,可以有向或者无向
权重Weight
从一个顶点到另一个顶点的代价
路径Path
由边依次连接起来的顶点序列。无权路径的长度为边的数量,带权是权重和
圈Cycle
首尾顶点相同的路径
V
顶点集合
E
边集合
e=( v , w )
E中每条边 e=( v , w ),v 和 w 都是V中顶点
子图
V 和 E 的子集
2 邻接矩阵
优点:简单,容易得到顶点如何连接。
缺点:边数少会变成稀疏矩阵。
3 邻接表
my::邻接表
维护一个包含所有顶点的主列表(master list),主列表中的每个顶点再关联一个与自身有边连接的所有顶点的列表。
class Vertex : def __init__ (self, key: any ): self.id = key self.connectedTo = {} def addNeighbor (self, nbr:object , weight=0 ) -> None : self.connectedTo[nbr] = weight def __str__ (self )->any : return '[{0}]' .format (str (self.id )) + '<->' + str ([x.id for x in self.connectedTo]) def getConnections (self )->any : return self.connectedTo.keys() def getId (self )->any : return self.id def getWeight (self,nbr:any )->any : return self.connectedTo[nbr] class Graph : def __init__ (self ): self.vertList = {} self.numVertices = 0 def addVertex (self,key:any )->Vertex: self.numVertices += 1 newVertex = Vertex(key) self.vertList[key] = newVertex return newVertex def getVertex (self,vKey:any )->any : if vKey in self.vertList: return self.vertList[vKey] else : return None def __contains__ (self, vKey:any )->bool : return vKey in self.vertList def addEdge (self,fromVert:any ,toVert:any ,weight:any )->None : if fromVert not in self.vertList: newVertex = self.addVertex(fromVert) if toVert not in self.vertList: newVertex = self.addVertex(toVert) self.vertList[fromVert].addNeighbor(self.vertList[toVert],weight) def Vertices (self )->list : return list (self.vertList.keys()) def __iter__ (self )->iter : return iter (self.vertList.values())
4 骑士周游问题
哈密顿通路(回路)与哈密顿图 (Hamilton图) 通过图G的每个结点一次,且仅一次的通路(回路),就是哈密顿通路(回路). 存在哈密顿回路 的图就是哈密顿图。
利用深度优先:O(k ** N)(N是棋盘格数)
改进:启发式规则:预先知道下一步的格子中的选择有多少种,优先选择下一个格子的选择少的那个格子。
5 拓扑排序
处理一个DAG,输出顶点的线性序列。广泛应用于依赖事件的排期上。(有先后顺序的多个事件)。
利用 dfs_visit 从某点出发遍历后续事件,得到事件一个列表。
6 并查集
并查集(Union-find Sets)是一种非常精巧而实用的数据结构,它主要用于处理一些不相交集合的合并问题。一些常见的用途有求连通子图、求最小生成树的Kruskal算法和求最近公共祖先(LCA)等。
结构:
init:初始化,让每个结点的父亲都指向自己。
find:递归寻找指向自己的结点,该结点则为传入结点的最终父亲。
union:修改结点指向,合并结点。
find路径压缩:将链表转化成树,每个结点直接指向最终的父亲,不需要再一个个遍历。
例题:
fa = [0 for i in range (n + 1 )] def init (n:int )->None : for i in range (1 ,n + 1 ): fa[i] = i find(i:int )->int : if fa[i] == i: return i else : return find(fa[i]) def union (i:int ,j:int )->None : i_fa = find(i) j_fa = find(j) fa[i_fa] = j_fa int find(i:int ): if fa[i] == i: return i else : fa[i] = find(fa[i]) return fa[i]
7 最小生成树
Prim算法:
Kruskal算法:
import heapqdef prim (graph ): n = len (graph) visited = [False ] * n min_heap = [(0 , 0 )] min_cost = 0 while min_heap: cost, node = heapq.heappop(min_heap) if visited[node]: continue visited[node] = True min_cost += cost for neighbor, weight in graph[node]: if not visited[neighbor]: heapq.heappush(min_heap, (weight, neighbor)) return min_cost
class UnionFind : def __init__ (self, n ): self.parent = list (range (n)) self.rank = [0 ] * n def find (self, x ): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union (self, x, y ): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return False if self.rank[root_x] < self.rank[root_y]: self.parent[root_x] = root_y elif self.rank[root_x] > self.rank[root_y]: self.parent[root_y] = root_x else : self.parent[root_y] = root_x self.rank[root_x] += 1 return True def kruskal (graph ): edges = [] n = len (graph) for i in range (n): for j, weight in graph[i]: edges.append((weight, i, j)) edges.sort() uf = UnionFind(n) min_cost = 0 num_edges = 0 for weight, u, v in edges: if uf.union(u, v): min_cost += weight num_edges += 1 if num_edges == n - 1 : break return min_cost
八、python算法 1 最大公约数
欧几里得算法 / 辗转相除法:
def gcd (num1,num2 ): while num2 != 0 : temp = num1 num1 = num2 num2 = temp % num2 return num1
2 递归
模拟树
数量统计递归,可以从上到下传递一个值,也可以从下到上返回一个值。同时维护一个全局变量,每次更新值就更新全局变量。(最大或最小)
递归三要素:
递归算法必须有一个基本结束条件。(最小规模问题的直接解决)
递归算法必须能改变状态向基本结束条件演进。(减小问题规模)
递归算法必须调用自身。(解决减小了规模的相同问题)
master主定理
mast主定理的递归公式:$T(n)=a*T(n/b)+n^d$,其中 a > 1 , b > 1 , d > 0。
$a < b^d $||$log_ba < d$,$T(n) = O(n^d)$
$a = b^d$ || $log_ba = d$,$T(n) = O(n^{log_ba}*log n)$
$a > b^d$ || $log_ba > d$,$T(n)=O(n^{log_ba})$
例题:
sys.getrecursionlimit() sys.setrecursionlimit(num) def add (nums:list )->int : if len (nums) == 1 : return nums[0 ] else : return nums[0 ] + add(nums[1 :]) def convertNum (num:int ,base:int =2 )->str : dic = {1 : '1' , 2 : '2' , 3 : '3' , 4 : '4' , 5 : '5' , 6 : '6' , 7 : '7' , 8 : '8' , 9 : '9' , 10 : 'A' , 11 : 'B' , 12 : 'C' , 13 : 'D' , 14 : 'E' , 15 : 'F' } if num < base: return dic[num] else : a,b = divmod (num,base) return convertNum(a,base) + str (b) import turtledef tree (t:turtle.Turtle,brance_len:int ): if brance_len > 5 : t.forward(brance_len) t.right(20 ) tree(t,brance_len-15 ) t.left(40 ) tree(t,brance_len-15 ) t.right(20 ) t.backward(brance_len) if __name__ == '__main__' : t = turtle.Turtle() t.left(90 ) t.penup() t.backward(100 ) t.pendown() t.pencolor('green' ) t.pensize(2 ) tree(t,75 ) t.hideturtle() turtle.done() import turtledef sierpinski (points, degree, myTurtle ): colormap = ['blue' , 'red' , 'green' , 'yellow' , 'white' , 'orange' ] draw_triangle(points, colormap[degree - 1 ], myTurtle) if degree > 0 : sierpinski([points[0 ], get_middle(points[0 ], points[1 ]), get_middle(points[0 ], points[2 ])], degree - 1 , myTurtle) sierpinski([points[1 ], get_middle(points[0 ], points[1 ]), get_middle(points[1 ], points[2 ])], degree - 1 , myTurtle) sierpinski([points[2 ], get_middle(points[2 ], points[1 ]), get_middle(points[0 ], points[2 ])], degree - 1 , myTurtle) def draw_triangle (points, color,myTurtle ): myTurtle.fillcolor(color) myTurtle.up() myTurtle.goto(points[0 ][0 ],points[0 ][1 ]) myTurtle.down() myTurtle.begin_fill() myTurtle.goto(points[1 ][0 ],points[1 ][1 ]) myTurtle.goto(points[2 ][0 ], points[2 ][1 ]) myTurtle.goto(points[0 ][0 ], points[0 ][1 ]) myTurtle.end_fill() def get_middle (p1,p2 ): return ((p1[0 ] + p2[0 ])/2 ,(p1[1 ] + p2[1 ])/2 ) if __name__ == '__main__' : myTurtle = turtle.Turtle() window = turtle.Screen() points = [[-200 ,-100 ],[0 ,200 ],[200 ,-100 ]] sierpinski(points,5 ,myTurtle) myTurtle.up() myTurtle.setpos(200 ,200 ) window.exitonclick()
3 回溯 3.1 普通回溯
同理于递归。
思路:当前操作?子问题?下一子问题?
时间复杂度为指数级别 $O(a^n)$,易超时。
例题:
3.2 子集型回溯
每个元素选或不选,也可以是选列表之间逗号,而非元素。
例题:
3.3 组合型回溯
组合问题。
剪枝:后续不存在满足条件的值将直接返回,不再递归。
例题:
4 分治
将问题分为若干更小规模的部分。通过解决每个小规模部分问题,最后解决大问题。
4.1 优化问题与贪心算法
贪心:每次都试图解决问题尽量大的一部分。(从顶到底依次最优)
例题:
def recMC (coinValueList:list ,change:int )->int : minCoins = change if change in coinValueList: return 1 else : for i in [c for c in coinValueList if c <= change]: numCoins = 1 + recMC((coinValueList),change - i) if numCoins < minCoins: minCoins = numCoins return minCoins def recDC (coinValueList:list ,change:int ,knownResults:list )->int : minCoins = change if change in coinValueList: knownResults[change] = 1 return 1 elif knownResults[change] > 0 : return knownResults[change] else : for i in [c for c in coinValueList if c <= change]: numCoins = 1 + recDC((coinValueList),change - i,knownResults) if numCoins < minCoins: minCoins = numCoins knownResults[change] = minCoins return minCoins print (recDC([1 ,5 ,10 ,21 ,25 ],63 ,[0 ]*64 ))
4.2 KMP算法
解决了字符串中找子串的问题。
时间复杂度:
author::KMP算法代码
def build_next (patt ): next = [0 ] prefix_len = 0 i = 1 while i < len (patt): if patt[prefix_len] == patt[i]: prefix_len += 1 next .append(prefix_len) i += 1 else : if prefix_len == 0 : next .append(0 ) i += 1 else : prefix_len = next [prefix_len - 1 ] return next def kmp_search (string,patt ): next = build_next(patt) i = 0 j = 0 while i < len (string): if string[i] == patt[j]: i += 1 j += 1 elif j > 0 : j = next [j - 1 ] else : i += 1 if j == len (patt): return i - j return -1
4.3 动态规划 [dp]
把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解。(从底到顶依次最优)
同理于回溯:当前操作?子问题?下一子问题?
例题:
def dpMakeChange (coinValueList:list ,change:int ,minCoins:list )->int : for cents in range (1 ,change + 1 ): coinCount = cents for j in [c for c in coinValueList if c <= cents ]: if minCoins[cents - j] + 1 < coinCount: coinCount = minCoins[cents - j] + 1 minCoins[cents] = coinCount return minCoins[change] print (dpMakeChange([1 ,5 ,10 ,21 ,25 ],63 ,[0 ]*64 ))def exdpMakeChange (coinValueList:list ,change:int ,minCoins:list ,coinsUsed:list )->int : for cents in range (1 ,change + 1 ): coinCount = cents newCoin = 1 for j in [c for c in coinValueList if c <= cents ]: if minCoins[cents - j] + 1 < coinCount: coinCount = minCoins[cents - j] + 1 newCoin = j minCoins[cents] = coinCount coinsUsed[cents] = newCoin return minCoins[change] def printCoins (coinsUsed,change ): coin = change while coin > 0 : thisCoin = coinsUsed[coin] print (thisCoin) coin = coin - thisCoin amnt = 63 clist = [1 ,5 ,10 ,21 ,25 ] coinsUsed = [0 ] * (amnt + 1 ) coinCount = [0 ] * (amnt + 1 ) print ("Making change for" ,amnt,"requires" )print (exdpMakeChange(clist,amnt,coinCount,coinsUsed),"coins" )print ("They are:" )printCoins(coinsUsed,amnt) print ("The used list is as follows:" )print (coinsUsed)
4.4 区间dp
从小区间转移到大区间。
例题:
4.5 数位dp
题型:给定一个闭区间 [ L , R ],求这个区间中满足 “某种条件” 的数的总量。
思路:$Ans[L,R] = Ans[1,R] - Ans[1,L-1]$(求出全部后减去下界)
5 查找 5.1 二分查找
bisect库
求最小中的最大值或者最大值中的最小值,一般可以考虑二分查找。
有序列表,中间开始找,然后依次缩小一半。
注意:明确 lp 和 rp 指向的区间是开还是闭区间,否则容易死循环。
例题:
import bisectbisect(ls,x) bisect_left(ls,x) bisec_right(ls,x) def binary_search (arr, low, high, x ): arr.sort() while low <= high: mid = (low + high) // 2 if arr[mid] == x: return mid elif arr[mid] > x: high = mid - 1 else : low = mid + 1 return -1
5.2 二叉搜索树
对一个节点来说,左子树的值都小于它,右子树的值都大于它。
前序遍历:根左右。
中序遍历:左根右。
后序遍历:左右根。
例题:
5.3 二叉树的最近公共祖先
获得左右值是否等于目标值,判断值存在以此判断祖先。
例题:
5.4 BFS
广度优先搜索。
时间:$O( |V| + |E| )$(节点数 + 边数)
为了跟踪顶点的加入过程,并避免重复顶点,要为顶点增加3个属性:
distance:从起始顶点到此顶点路径长度
predecessor:可反向追溯到起点
color:标识了此顶点是尚未发现(白色)、已经发现(灰色)、还是已经完成探索(黑色)
还需要用一个队列 Queue 来对已发现的顶点进行排列: 决定下一个要探索的顶点(队首顶点)
从起始顶点s开始,作为刚发现的顶点,标注为灰色,距离为0,前驱为None,加入队列,接下来是个循环迭代过程:
从队首取出一个顶点作为当前顶点。
遍历当前顶点的邻接顶点,如果是尚未发现的白色顶点,则将其颜色改为灰色(已发现),距离增加1,前驱顶点为当前顶点,加入到队列中。
遍历完成后,将当前顶点设置为黑色(已探索过),循环回到步骤1的队首取当前顶点。
例题:
5.5 DFS
深度优先搜索。
添加属性同理于BFS,过程是从顶点开始一直向下搜索,直到没有路径可以走且结束条件为满足时返回上一层,继续搜索。 还需要用一个栈 Stack 来记录探索过的顶点。(用于返回上一顶点)
有时候深度优先搜索会创建多棵树,称为 “深度优先森林” 时间:$O( |V| + |E| )$(dfs + dfsvisit)
6 排序
可视化排序
6.1 冒泡排序
冒泡排序:每次将最大或最小项置顶,不断交换位置,直到完成遍历。
改进:加一个exchanges = False,如果一趟循环未发生任何排序则保持False表示已经排好了序。
时间:$O(n^2)$
for i in range (len ): for j in range (i,len ): if lst[i] > lst[j]: lst[i],lst[j] = lst[j],lst[i]
6.2 选择排序
同理冒泡,但是一开始不做交换,只记录位置,到单次循环的最后一次才进行交换。就是每次选一个最大或最小的值放到最前面。
时间:$O(n^2)$
def selection_sort (arr ): n = len (arr) for i in range (n-1 ): min_index = i for j in range (i+1 , n): if arr[j] < arr[min_index]: min_index = j arr[i], arr[min_index] = arr[min_index], arr[i] return arr
6.3 插入排序
维持一个已经排好序的子列表,位置一直在列表前部,然后逐步扩大到这个子列表直到全表,例如抓牌。
时间:$O(n^2)$
def insertion_sort (arr ): n = len (arr) for i in range (1 , n): key = arr[i] j = i - 1 while j >= 0 and arr[j] > key: arr[j+1 ] = arr[j] j -= 1 arr[j+1 ] = key return arr
6.4 希尔排序
因为列表越有序,插入排序比对次数越少,所以希尔排序就是分块的插入排序。间隔一般为n / 2,例:选1,4,7项。
时间:$O(n^{\frac{2}{3}})$
6.5 归并排序
递归算法,将数据表持续分裂成两半,对两半分别进行归并排序。(稳定) 时间分裂:$O(logn)$,归并:$O(n)$,总:$O(nlogn)$,会牺牲空间
def merge_Sort (lst:list )->list : if len (lst) <= 1 : return lst mid = len (lst) // 2 left = merge_Sort(lst[:mid]) right = merge_Sort(lst[mid:]) merged = [] while left and right: if left[0 ] <= right[0 ]: merged.append(left.pop(0 )) else : merged.append(right.pop(0 )) merged.extend(right if right else left) return merged print (merge_Sort([93 ,54 ,12 ,74 ,21 ,5 ,78 ,22 ,94 ,34 ]))
6.6 快速排序
递归算法,依据一个”中值”数据项把数据表分为两半:小于中值的一半和大于中值的一半,然后再分别进行快速排序。(不稳定)
时间总能分裂:$O(logn)$,移动:$O(n)$,总:$O(nlogn)$
若中值偏离中部,时间:$O(n^2)$,因为递归会导致其不如冒泡,
def quickSort (lst:list ): quickSortHelper(lst,0 ,len (lst)-1 ) def quickSortHelper (lst:list ,first:int ,last:int )->None : if first < last: splitpoint = partition(lst,first,last) quickSortHelper(lst,first,splitpoint-1 ) quickSortHelper(lst,splitpoint+1 ,last) return None def partition (lst:list ,first:int ,last:int )->int : pivotvalue = lst[first] leftmark = first + 1 rightmark = last done = False while not done: while leftmark <= rightmark and lst[leftmark] <= pivotvalue: leftmark = leftmark + 1 while lst[rightmark] >= pivotvalue and rightmark >= leftmark: rightmark = rightmark - 1 if rightmark < leftmark: done = True else : lst[leftmark],lst[rightmark] = lst[rightmark],lst[leftmark] lst[first],lst[rightmark] = lst[rightmark],lst[first] return rightmark lst = [54 ,26 ,93 ,17 ,77 ,31 ,44 ,55 ,20 ] quickSort(lst) print (lst)
6.7 堆排序
二叉堆的排序。
时间:$O(nlogn)$
def heapify (arr, n, i ): largest = i left = 2 * i + 1 right = 2 * i + 2 if left < n and arr[left] > arr[largest]: largest = left if right < n and arr[right] > arr[largest]: largest = right if largest != i: arr[i], arr[largest] = arr[largest], arr[i] heapify(arr, n, largest) def heap_sort (arr ): n = len (arr) for i in range (n//2 - 1 , -1 , -1 ): heapify(arr, n, i) for i in range (n-1 , 0 , -1 ): arr[i], arr[0 ] = arr[0 ], arr[i] heapify(arr, i, 0 ) return arr
6.8 桶排序
def bucket_sort (arr, num_buckets ): max_val = max (arr) min_val = min (arr) bucket_range = (max_val - min_val) / num_buckets buckets = [[] for _ in range (num_buckets)] for num in arr: index = int ((num - min_val) // bucket_range) if index == num_buckets: index -= 1 buckets[index].append(num) sorted_arr = [] for bucket in buckets: sorted_arr.extend(sorted (bucket)) return sorted_arr
6.9 基数排序
def radix_sort (arr ): max_val = max (arr) exp = 1 while max_val // exp > 0 : counting_sort_by_digit(arr, exp) exp *= 10 return arr def counting_sort_by_digit (arr, exp ): count = [0 ] * 10 sorted_arr = [0 ] * len (arr) for num in arr: index = (num // exp) % 10 count[index] += 1 for i in range (1 , 10 ): count[i] += count[i-1 ] for i in range (len (arr)-1 , -1 , -1 ): index = (arr[i] // exp) % 10 sorted_arr[count[index]-1 ] = arr[i] count[index] -= 1 for i in range (len (arr)): arr[i] = sorted_arr[i]
6.10 计数排序
流程:
找出待排序数组中的最大值 maxVal
和最小值 minVal
。
创建一个长度为 maxVal - minVal + 1
的计数数组 countArray
,用于统计每个元素的出现次数。
遍历待排序数组,统计每个元素出现的次数并存储到 countArray
中。
计算每个元素在排序后的数组中的起始位置(前缀和),修改 countArray
使其表示元素应该放置的位置。
创建一个与待排序数组相同大小的结果数组 resultArray
。
遍历待排序数组,根据元素的值和 countArray
中记录的位置信息,将元素放入 resultArray
的正确位置。
返回 resultArray
即为排序后的数组。
时间:
空间:
def countingSort (arr ): maxVal = max (arr) minVal = min (arr) countArray = [0 ] * (maxVal - minVal + 1 ) for num in arr: countArray[num - minVal] += 1 for i in range (1 , len (countArray)): countArray[i] += countArray[i - 1 ] resultArray = [0 ] * len (arr) for num in arr: index = countArray[num - minVal] - 1 resultArray[index] = num countArray[num - minVal] -= 1 return resultArray
7 条件 7.1 单调栈
场景:前值小于后值的最小索引位。
例子:
7.2 双指针
同向双指针:
又称为滑动窗口。
场景:找主串中的子串,满足条件时移动右指针,不满足条件时移动左指针。
方法:设置两个索引找到字符串中的两个位置。
同向双指针例题:
同向双指针习题:
——————————————————————
相向双指针:
场景:有序列表中找两数之和,中间围成的面积。
方法:双指针,但是从头尾开始。
相向双指针例题:
7.3 前缀和
场景:需要用到数组中的前几项和。
方法:相加前几项存入新数组中。
例题:
7.4 二进制位图
场景:题目需求为两种情况时。
方法:设置二进制数来代表状态。
例题:
7.5 模运算
场景:乘除取指定数字位数。
方法:
$(a+b)MODm=((aMODm)+(bMODm))MODm$
$(ab)MODm=((aMODm) (bMODm))MODm$
例题: